/**
 *
 */
package br.com.doclayer.service.cmis;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.chemistry.opencmis.client.api.CmisObject;
import org.apache.chemistry.opencmis.client.api.Document;
import org.apache.chemistry.opencmis.client.api.FileableCmisObject;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.client.api.ObjectId;
import org.apache.chemistry.opencmis.client.api.OperationContext;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.util.FileUtils;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConnectionException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.impl.MimeTypes;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.commons.io.FilenameUtils;

import br.com.doclayer.AddContentMode;
import br.com.doclayer.ContentMetadata;
import br.com.doclayer.FolderMetadata;
import br.com.doclayer.ItemIterable;
import br.com.doclayer.ObjectMetadata;
import br.com.doclayer.cmis.ContentCMISImpl;
import br.com.doclayer.cmis.FolderCMISImpl;
import br.com.doclayer.cmis.ItemIterableImpl;
import br.com.doclayer.cmis.ObjectCMISImpl;
import br.com.doclayer.exceptions.DocLayerCommunicationException;
import br.com.doclayer.exceptions.DocLayerContentNotFoundException;
import br.com.doclayer.exceptions.DocLayerDuplicateContentException;
import br.com.doclayer.service.DocService;

/**
 * @author helder
 * @author Glauco
 *
 */
public class DocServiceCMISImpl implements DocService {

  private Session docServiceSession;

  private String username;
  private String password;
  private String url;
  private String repositoryId;

  /**
   * @param username
   * @param password
   * @param url
   * @param repositoryId
   */
  public DocServiceCMISImpl(String username, String password, String url,
      String repositoryId) {
    super();
    this.username = username;
    this.password = password;
    this.url = url;
    this.repositoryId = repositoryId;
  }

  /*
   * (non-Javadoc)
   *
   * @see br.com.impactots.ged.service.GEDService#addFolder(java.lang.String,
   * java.lang.String)
   */
  @Override
  public FolderMetadata createFolder(String parentFolderUuid,
      String newFolderName) throws DocLayerCommunicationException,
      DocLayerDuplicateContentException {
    try {
      Session session = this.getDocServiceSession();

      Folder folder = FileUtils.createFolder(parentFolderUuid, newFolderName,
          null, session);

      return new FolderCMISImpl(folder);
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisConstraintException e) {
      throw new DocLayerDuplicateContentException(
          "Já existe outra pasta com o mesmo nome.", e);
    }
  }

  @Override
  public FolderMetadata createFolderTree(String folderPath)
      throws DocLayerCommunicationException, DocLayerDuplicateContentException {
    try {
      Session session = this.getDocServiceSession();
      Folder folder = this.createFolderIfNotExists(folderPath, session);
      return new FolderCMISImpl(folder);
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisConstraintException e) {
      throw new DocLayerDuplicateContentException(
          "Já existe outra pasta com o mesmo nome.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see br.com.impactots.ged.service.GEDService#addContent(java.io.File,
   * java.lang.String, java.lang.String[])
   */
  @Override
  public ContentMetadata addContent(File file, AddContentMode addContentMode,
      String... foldersUuid) throws DocLayerCommunicationException,
      DocLayerDuplicateContentException, DocLayerContentNotFoundException,
      FileNotFoundException {
    String contentName = file.getName();
    long contentSize = file.length();
    InputStream inputStream = new FileInputStream(file);

    return this.addContent(inputStream, contentName, contentSize, false,
        addContentMode, foldersUuid);
  }

  private String validateContentName(String contentName,
      AddContentMode addContentMode, String... foldersUuid)
      throws DocLayerCommunicationException, DocLayerDuplicateContentException {
    String filename = contentName;

    switch (addContentMode) {
    case OVERWRITE:
      try {
        for (String folderPath : foldersUuid) {
          if (this.contentExists(folderPath + "/" + filename)) {
            this.removeObject(folderPath + "/" + filename);
          }
        }
      } catch (DocLayerContentNotFoundException e) {
      }
      break;
    case APPEND_SUFFIX:
      int suffix = 0;
      for (int i = 0; i < foldersUuid.length; i++) {
        String folderPath = foldersUuid[i];
        if (!folderPath.endsWith("/")) {
          folderPath = folderPath + "/";
        }

        if (this.contentExists(folderPath + filename)) {
          suffix++;
          String baseName = FilenameUtils.getBaseName(contentName);
          String extension = FilenameUtils.getExtension(contentName);
          filename = baseName + " - "
              + this.leftPad(String.valueOf(suffix), 3, '0') + "." + extension;
          i = 0;
        }
      }
      break;
    default:
      for (String folderPath : foldersUuid) {
        if (this.contentExists(folderPath + "/" + filename)) {
          throw new DocLayerDuplicateContentException(
              "Já existe outro arquivo com o mesmo nome.");
        }
      }
      break;
    }

    return filename;
  }

  @Override
  public ContentMetadata addContent(InputStream inputStream,
      String contentName, long contentSize, boolean createFolder,
      AddContentMode addContentMode, String... foldersUuid)
      throws DocLayerCommunicationException, DocLayerDuplicateContentException,
      DocLayerContentNotFoundException {
    if (foldersUuid.length == 0) {
      throw new IllegalArgumentException(
          "Deve ser informada no mínimo 1 (uma) pasta para o arquivo.");
    }

    try {
      String filename = this.validateContentName(contentName, addContentMode,
          foldersUuid);
      // Cria o documento na primeira pasta especificada (pasta pricipal)
      Document document = this.addDocument(inputStream, filename, contentSize,
          foldersUuid[0], createFolder);

      // Se houver mais pastas especificadas, adiciona referencias para o
      // documento
      if (foldersUuid.length > 1) {
        Session session = this.getDocServiceSession();
        for (int i = 1; i < foldersUuid.length; i++) {
          Folder parentFolder = null;
          if (createFolder) {
            parentFolder = this
                .createFolderIfNotExists(foldersUuid[i], session);
          } else {
            parentFolder = FileUtils.getFolder(foldersUuid[i], session);
          }
          document.addToFolder(parentFolder, true);
        }
      }

      return new ContentCMISImpl(document);
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisConstraintException e) {
      throw new DocLayerDuplicateContentException(
          "Já existe outro arquivo com o mesmo nome.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Pasta não encontrada.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see br.com.impactots.ged.service.GEDService#removeObject(java.lang.String)
   */
  @Override
  public void removeObject(String pathOrIdOfObject)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    try {
      FileUtils.delete(pathOrIdOfObject, this.getDocServiceSession());
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Objeto não encontrado.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see br.com.impactots.ged.service.GEDService#removeObject(br.com.impactots
   * .ged.ObjectMetadata)
   */
  @Override
  public void removeObject(ObjectMetadata objectMetadata)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    this.removeObject(objectMetadata.getUuid());
  }

  public ItemIterable<ContentMetadata> findContent(String where) {
    Session session = this.getDocServiceSession();
    OperationContext operationContext = session.createOperationContext(null,
        false, true, false, IncludeRelationships.NONE, null, true, null, true,
        50);

    return new ItemIterableImpl<ContentMetadata>(session.queryObjects(
        BaseTypeId.CMIS_DOCUMENT.value(), where, false, operationContext));
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * br.com.impactots.ged.service.GEDService#getObjectMetadata(java.lang.String
   * )
   */
  @Override
  public ObjectMetadata getObjectMetadata(String pathOrUuid)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    try {
      Session session = this.getDocServiceSession();

      CmisObject object = FileUtils.getObject(pathOrUuid, session);

      return this.instantiateObjectMetadata(object);
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Objeto não encontrado.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see br.com.impactots.ged.service.GEDService#getContentMetadata(java.lang.
   * String)
   */
  @Override
  public ContentMetadata getContentMetadata(String pathOrUuid)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    ObjectMetadata objectMetadata = this.getObjectMetadata(pathOrUuid);

    if (objectMetadata instanceof ContentMetadata) {
      return (ContentMetadata) objectMetadata;
    } else {
      throw new IllegalArgumentException(
          "O UUID informado, não é de um documento.");
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * br.com.impactots.ged.service.GEDService#getFolderMetadata(java.lang.String
   * )
   */
  @Override
  public FolderMetadata getFolderMetadata(String pathOrUuid)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    ObjectMetadata objectMetadata = this.getObjectMetadata(pathOrUuid);

    if (objectMetadata instanceof FolderMetadata) {
      return (FolderMetadata) objectMetadata;
    } else {
      throw new IllegalArgumentException(
          "O caminho ou UUID informado não é de uma pasta.");
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * br.com.impactots.ged.service.GEDService#getFolderChildren(java.lang.String
   * )
   */
  @Override
  public List<ObjectMetadata> getFolderChildren(String pathOrIdOfFolder)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    try {
      Folder folder = FileUtils.getFolder(pathOrIdOfFolder,
          this.getDocServiceSession());

      List<ObjectMetadata> children = new ArrayList<ObjectMetadata>();
      for (CmisObject child : folder.getChildren()) {
        children.add(this.instantiateObjectMetadata(child));
      }

      return children;
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Pasta não encontrada.", e);
    }
  }

  @Override
  public void moveFolderContents(String origFolder, String destFolder,
      boolean removeOrigFolder) throws DocLayerCommunicationException,
      DocLayerContentNotFoundException {
    try {
      Session session = this.getDocServiceSession();

      Folder originalFolder = FileUtils.getFolder(origFolder, session);
      Folder destinationFolder = this.createFolderIfNotExists(destFolder,
          session);

      for (CmisObject child : originalFolder.getChildren()) {
        if (child instanceof FileableCmisObject) {
          FileableCmisObject fileableChild = (FileableCmisObject) child;
          fileableChild.move(originalFolder, destinationFolder);
        }
      }

      if (removeOrigFolder) {
        this.removeObject(originalFolder.getId());
      }
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Objeto não encontrado.", e);
    }
  }

  @Override
  public long getFolderSize(String pathOrUuid)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    try {
      Folder folder = FileUtils.getFolder(pathOrUuid,
          this.getDocServiceSession());

      Set<String> uuids = new HashSet<String>();
      return this.calcFolderSize(folder, uuids);
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Pasta não encontrada.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * br.com.impactots.ged.service.GEDService#getContentStream(java.lang.String )
   */
  @Override
  public InputStream getContentStream(String pathOrIdOfContent)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    try {
      Session session = this.getDocServiceSession();

      CmisObject document = FileUtils.getObject(pathOrIdOfContent, session);

      if (document instanceof Document) {
        return ((Document) document).getContentStream().getStream();
      } else {
        throw new IllegalArgumentException(
            "O caminho ou UUID informado não é de um documento.");
      }
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
      throw new DocLayerContentNotFoundException("Documento não encontrado.", e);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see
   * br.com.impactots.ged.service.GEDService#getContentStream(br.com.impactots
   * .ged.ContentMetadata)
   */
  @Override
  public InputStream getContentStream(ContentMetadata contentMetadata)
      throws DocLayerCommunicationException, DocLayerContentNotFoundException {
    return this.getContentStream(contentMetadata.getUuid());
  }

  /**
   * Cria um conteúdo no repositório e retorna uma instância de Document, a
   * partir de um objeto da classe File, na pasta representada pelo uuid ou path
   * passado através do parâmetro parentFolderUuid
   *
   * @param file
   *          conteúdo a ser criado no repositório
   * @param parentFolderUuid
   *          uuid ou path da pasta destino
   * @return instância de Document da API opencmis client
   * @throws IOException
   */

  private Document addDocument(InputStream inputStream, String contentName,
      long contentSize, String parentFolderUuid, boolean createFolder) {
    Session session = this.getDocServiceSession();

    String type = BaseTypeId.CMIS_DOCUMENT.value(); // "cmis:document";

    Folder parentFolder = null;
    if (createFolder) {
      parentFolder = this.createFolderIfNotExists(parentFolderUuid, session);
    } else {
      parentFolder = FileUtils.getFolder(parentFolderUuid, session);
    }

    String mimetype = MimeTypes.getMIMEType(contentName);

    Map<String, Object> properties = new HashMap<String, Object>();
    properties.put(PropertyIds.OBJECT_TYPE_ID, type);
    properties.put(PropertyIds.NAME, contentName);

    ContentStream contentStream = new ContentStreamImpl(contentName,
        BigInteger.valueOf(contentSize), mimetype, inputStream);

    return parentFolder.createDocument(properties, contentStream,
        VersioningState.MAJOR);
  }

  private Folder createFolderIfNotExists(String folderPath, Session session) {
    String[] folders = folderPath.split("/");
    String parentFolder = "/";
    Folder resultFolder = null;
    int i = 0;
    if (folderPath.startsWith("/")) {
      i = 1;
    }
    for (; i < folders.length; i++) {
      try {
        resultFolder = FileUtils.getFolder(parentFolder + folders[i], session);
        if (resultFolder == null) {
          resultFolder = FileUtils.createFolder(parentFolder, folders[i], null,
              session);
        }
      } catch (CmisObjectNotFoundException e) {
        resultFolder = FileUtils.createFolder(parentFolder, folders[i], null,
            session);
      }
      parentFolder = parentFolder + folders[i] + "/";
    }
    return resultFolder;
  }

  /**
   * Retorna uma instância de ObjectId referente ao uuid passado por parâmetro
   *
   * @param uuid
   *          identificador universal do objeto
   * @return instância de ObjectId referente ao uuid passado
   */
  @SuppressWarnings("unused")
  private ObjectId getObjectId(String uuid) {
    return this.getDocServiceSession().createObjectId(uuid);
  }

  private boolean contentExists(String fullpath)
      throws DocLayerCommunicationException {
    try {
      Session session = this.getDocServiceSession();
      if (session.getObjectByPath(fullpath) != null) {
        return true;
      }
    } catch (CmisConnectionException e) {
      throw new DocLayerCommunicationException(
          "Falha na comunicação com o servidor.", e);
    } catch (CmisObjectNotFoundException e) {
    }
    return false;
  }

  private ObjectMetadata instantiateObjectMetadata(CmisObject object) {
    ObjectMetadata result;

    if (object instanceof Folder) {
      result = new FolderCMISImpl((Folder) object);
    } else if (object instanceof Document) {
      result = new ContentCMISImpl((Document) object);
    } else {
      result = new ObjectCMISImpl(object);
    }

    return result;
  }

  private long calcFolderSize(Folder folder, Set<String> uuids) {
    long size = 0;

    for (CmisObject object : folder.getChildren()) {
      if (object instanceof Document) {
        Document document = (Document) object;
        if ((document.getContentStreamLength() > 0)
            && (!uuids.contains(document.getId()))) {
          uuids.add(document.getId());
          size = size + document.getContentStreamLength();
        }
      } else if (object instanceof Folder) {
        Folder childFolder = (Folder) object;
        size = size + this.calcFolderSize(childFolder, uuids);
      }
    }

    return size;
  }

  /**
   * @return Acessa o valor de docServiceSession.
   */
  public Session getDocServiceSession() {
    if (this.docServiceSession == null) {
      this.docServiceSession = DocServiceSessionManager.getSession(
          this.username, this.password, this.url, this.repositoryId);
    }
    return this.docServiceSession;
  }

  /**
   * @param docServiceSession
   *          Altera o valor de docServiceSession.
   */
  public void setDocServiceSession(Session docServiceSession) {
    this.docServiceSession = docServiceSession;
  }

  private String leftPad(String str, int size, char padChar) {
    if (str == null) {
      return null;
    }
    int pads = size - str.length();
    if (pads <= 0) {
      return str; // returns original String when possible
    }
    StringBuilder buf = new StringBuilder(pads);
    for (int i = 0; i < pads; i++) {
      buf.append(padChar);
    }
    return buf.toString().concat(str);
  }

}
